/*
 *   Servlet for surfacing syndicated content
 *   Copyright (C) 2010 Glenn Engstrand
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 * 
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.dynamicalsoftware.news;

import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.*;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Random;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.nio.charset.*;
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Attr;
import org.w3c.dom.html.HTMLTitleElement;
import org.w3c.dom.Text;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.dynamicalsoftware.news.parser.*;
import com.meterware.httpunit.*;

/**
 * Servlet implementation class NewsWidget
 */
public class NewsWidget extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private static final Logger log = Logger.getLogger(NewsWidget.class.getCanonicalName());
	
    private NewsWidgetActivity activity = null;
    
    /**
     * @see HttpServlet#HttpServlet()
     */
    public NewsWidget() {
        super();
    }

	@Override
	public void destroy() {
		if (activity != null) {
			activity.shutdown();
		}
		super.destroy();
	}

	@Override
	public void init() throws ServletException {
		super.init();
		activity = new NewsWidgetActivity();
		activity.start();
	}
	
	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
    @Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		PrintWriter writer = response.getWriter();
		String qs = request.getQueryString();
		qs = URLDecoder.decode(qs, Charset.defaultCharset().name());
		try {
			writer.write("<html>");
			HashMap<String, String> req = parseAST(qs);
			NodeList items = serviceRequest(req, writer);
			reportResults(items, req, writer);
		} catch (RecognitionException e) {
			displayError(e.getLocalizedMessage(), writer);
			log.log(Level.SEVERE, "parse error with: " + qs, e);
		} catch (Exception e) {
			displayError(e.getLocalizedMessage(), writer);
			log.log(Level.SEVERE, "exception parsing: " + qs, e);
		} finally {
			writer.write("</body></html>");
		}
	}
	
    /**
     * display an error message in the GUI
     * @param msg to be displayed
     * @param writer for the servlet output
     */
	private void displayError(String msg, PrintWriter writer) {
		writer.write("<p>" + msg + "</p>");
	}
	
	/**
	 * display a matched item from the syndication feed
	 * @param title is the short intro that serves as the clickable link text
	 * @param description (if available) is an introductory paragraph
	 * @param link is the URL to the destination address
	 * @param format indicates what format that this item should be displayed in
	 * @param writer for the servlet output
	 */
	private void displayItem(String title, String description, String link, String format, PrintWriter writer) {
		writer.write("<div class=\"" + format + "-item\">");
		writer.write("<a href=\"" + link + "\">" + title + "</a>");
		writer.write("<p>" + description + "</p></div>");
	}

	/**
	 * retrieves embedded text from the DOM
	 * @param child node that contains the embedded text
	 * @return text content of interest
	 * @throws DOMException
	 */
	private String getText(Node child) throws DOMException {
		String retVal = "";
		// sometimes the text is in this node
		retVal = child.getTextContent();
		if ((retVal == null) || "".equals(retVal)) {
			// and sometimes it is in the following node
			Node n = child.getNextSibling();
			if (n != null) {
				if (n instanceof Text) {
					Text t = (Text)n;
					retVal = t.getData();
				}
			}
		}
		return retVal;
	}
	
	/**
	 * test to determine whether or not the next item should be displayed
	 * @param already is the number of items that have already been displayed
	 * @param total is the total number of items to be displayed
	 * @param available is the total number of items that can be displayed
	 * @param gen is used to generate the next pseudo random number
	 * @return true if this item should be displayed
	 */
	private boolean shouldDisplay(int already, int total, int available, Random gen) {
		boolean retVal = true;
		// don't display more than we should
		if (already < total) {
			// display all if there aren't even enough
			if (available > total) {
				// use the random number generator for an even distribution
				retVal = ((Math.abs(gen.nextInt()) % available) < total);
				// I noticed that the first one is always the same
				if (already == 0) {
					retVal = ((Math.abs(gen.nextInt()) % 2) == 0);
				}
			}
		} else {
			retVal = false;
		}
		return retVal;
	}
	
	/**
	 * build the display for the items that were retrieved
	 * and store those items in cassandra
	 * @param items is the list of items retrieved
	 * @param reqParam contains all of the originally specified query
	 * @param writer for the servlet output
	 */
	private synchronized void reportResults(NodeList items, HashMap<String, String> reqParam, PrintWriter writer) {
		String title = "";
		String description = "";
		String link = "";
		Date now = new Date();        
        String key = DateFormat.getInstance().format(now) + " " + new Long(now.getTime()).toString();
		Random r = new Random(now.getTime());
		int count = 3;
		int currentItem = 0;
		String cnvCount = reqParam.get("count:");
		if ((cnvCount != null) && (cnvCount.length() > 0)) {
			try {
				count = Integer.parseInt(cnvCount);
			} catch (NumberFormatException nfe) {
				// do nothing
			}
		}
		String fid = reqParam.get("format:");
		int most = items.getLength();
		for (int i = 0; i < most; i++) {
			Node item = items.item(i);
			NodeList children = item.getChildNodes();
			for (int j = 0; j < children.getLength(); j++) {
				Node child = children.item(j);
				String tagName = child.getNodeName();
				// the title usually is parsed into a special element
				if (tagName.equalsIgnoreCase("title") && "".equals(title)) {
					if (child instanceof HTMLTitleElement) {
						HTMLTitleElement e = (HTMLTitleElement)child;
						title = e.getText();
					} else {
						title = child.getTextContent();
					}
					// depending on feed format, the description may get parsed into many different elements or not at all
				} else if (tagName.equalsIgnoreCase("description") && "".equals(description)) {
					description = getText(child);
				} else if (tagName.equalsIgnoreCase("summary") && "".equals(description)) {
					description = getText(child);
				} else if (tagName.equalsIgnoreCase("content") && "".equals(description)) {
					if (child instanceof Element) {
						Element e = (Element)child;
						NodeList blocks = e.getElementsByTagName("blockquote");
						for (int k = 0; k < blocks.getLength(); k++) {
							description = getText(blocks.item(k));
						}
					}
					// depending on feed format, the URL may be in a link body or as an href attribute
				} else if (tagName.equalsIgnoreCase("link") && "".equals(link)) {
					link = getText(child);
					if ((link == null) || (link.length() == 0)) {
						Node n = child.getAttributes().getNamedItem("href");
						if (n != null) {
							if (n instanceof Attr) {
								Attr a = (Attr)n;
								link = a.getValue();
							}
						}
					}
				}
			}
			if (shouldDisplay(currentItem, count, most, r)) {
				currentItem += 1;
				displayItem(title, description, link, fid, writer);
				// appending the current item number makes the key truly unique
				String ck = key + " - " + new Integer(currentItem).toString();
				if (activity == null) {
					log.log(Level.SEVERE, "Cannot log display activity as it has not been set up yet.");
				} else {
					activity.publish(ck, title, link, description);					
				}
				description = "";
				link = "";
				title = "";
			}
		}
	}
	
	/**
	 * fetch the corresponding syndication feed 
	 * @param reqParam the parsed parameters from the request
	 * @param writer for the servlet output
	 * @return the list of candidate items to display
	 * @throws IOException
	 * @throws SAXException
	 */
	private NodeList serviceRequest(HashMap<String, String> reqParam, PrintWriter writer) throws IOException, SAXException {
		String fid = reqParam.get("format:");
		// the servlet contains initialization data to map format to stylesheet
		String format = getInitParameter(fid);
		if ((format == null) || (format.length() == 0)) {
			format = "default.css";
		}
		String topic = reqParam.get("topic:");
		writer.write("<head><title>" + topic + "</title><link rel=\"stylesheet\" type=\"text/css\" HREF=\"" + format + "\" /></head><body>");
		// the servlet contains initialization data to map repository to syndication feed URL
		String url = getInitParameter(reqParam.get("repository:"));
		if ((url == null) || (url.length() == 0)) {
			writer.write("<p>Cannot find repository for: " + reqParam.get("repository:") + "</p>");
		}
		HttpUnitOptions.setScriptingEnabled(false);
		WebConversation wc = new WebConversation();
		WebRequest req = new GetMethodWebRequest(url);
		WebResponse resp = wc.getResponse(req);
		Document d = resp.getDOM();
		// syndication feed format (RSS or ATOM) determines what tag to look for
		NodeList items = d.getElementsByTagName("entry");
		if (items.getLength() == 0) {
			items = d.getElementsByTagName("item");
		}
		return items;
	}
	
	/**
	 * parse the query string and extract out the parameters
	 * @param cmd the query string to be parsed
	 * @return the set of parameters extracted from the query string
	 * @throws RecognitionException
	 * @throws IOException
	 * @throws SAXException
	 */
	private HashMap<String, String> parseAST(String cmd) throws RecognitionException, IOException, SAXException {
		ANTLRStringStream input = new ANTLRStringStream(cmd);
		NewsRequestASTLexer lex = new NewsRequestASTLexer(input);
		CommonTokenStream tokens = new CommonTokenStream(lex);
		NewsRequestASTParser parser = new NewsRequestASTParser(tokens);
		HashMap<String, String> reqParam = new HashMap<String, String>();
		Tree t = (Tree)parser.ask().getTree();
		for (int i=0; i<t.getChildCount(); i++) {
			Tree child = t.getChild(i);
			String param = child.getText();
			for (int j=0; j<child.getChildCount(); j++) {
				String value = child.getChild(j).getText();
				// extract the double quotes from the string values
				if (value.startsWith("\"") && value.endsWith("\"")) {
					value = value.substring(1, value.length() - 1);
				}
				if (reqParam.containsKey(param)) {
					String v = reqParam.get(param);
					v = v + "," + value;
					reqParam.put(param, v);
				} else {
					reqParam.put(param, value);					
				}
			}
		}
		return reqParam;
	}

}
